package saci.util.xml;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import saci.util.Types;

class XmlReader {

    static Logger logger = Logger.getLogger(XmlReader.class.getName());
    private static final char TAG_START = '<';
    private static final char TAG_END = '>';
    private static final char EQUAL = '=';
    private static final char CLOSE = '/';
    private static final char STRING_DELIMITER = '\"';
    private static final char NULL = '\0';
    private static final char COMMENT = '!';
    private static final String TAG_MAP_TAG_NAME = "__tagName";
    private static final String TAG_MAP_TAG_CLOSE = "__close";
    private int length = 0, position = 0;
    private char[] buffer = new char[128*1024];
    private Map<String, String> tagMap = new HashMap<String, String>();
    private InputStream inputStream;
    private Reader reader;
    private StringBuilder temp = new StringBuilder();
    private XmlMapper mapper = new XmlMapper();

    class ComplexType {

        Class<?> type;
        @SuppressWarnings("unchecked")
        List list;
    }

    public XmlReader(InputStream inputStream) throws IOException {
        this.inputStream = inputStream;
        init();
    }
    
    private void init() throws IOException {
    	Charset charset = null;
    	boolean xmlTag = false;
    	byte[] byteBuffer = new byte[512];
    	int length = inputStream.read(byteBuffer);
        ByteBuffer bb = ByteBuffer.allocate(byteBuffer.length);
        bb.put(byteBuffer);
        SortedMap<String, Charset> availableCharsets = Charset.availableCharsets();
        for (Entry<String,Charset> entry : availableCharsets.entrySet()) {
        	bb.rewind();
        	Charset tempCharset = entry.getValue();
        	String s = tempCharset.decode(bb).toString();
        	if (s.indexOf("<?") > -1) {
        		int indice = s.indexOf("?>");
        		if (indice > -1) {
	        		s = s.substring(0,indice);
	        		if ((indice = s.indexOf("encoding")) > -1) {
	        			int i = s.indexOf("\"", indice);
	        			int j = s.indexOf("\"", i+1);
	        			String encoding = s.substring(i+1, j);
        				charset = Charset.forName(encoding);
	        		}
	        		xmlTag = true;
	        		break;
        		}
        	}
        }
        if (charset == null) {
        	charset = Charset.defaultCharset();
        }
    	this.reader = new InputStreamReader(inputStream, charset);
        // Preenche o buffer principal
        InputStreamReader in = new InputStreamReader(new ByteArrayInputStream(byteBuffer), charset);
        this.length = in.read(this.buffer, 0, length);
        this.length += this.reader.read(this.buffer, this.length, this.buffer.length - this.length);
        this.position = 0;
        if (xmlTag) {
        	readTag(); // pula a tag inicial <? xml ... ?>
        }
    }

    private boolean fillBuffer() {
        try {
            length = reader.read(buffer);
            position = 0;
            return length > 0;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private char read() {
        if (position >= length) {
            boolean filled = fillBuffer();
            if (!filled) {
                throw new RuntimeException("Invalid XML");
            }
        }
        return buffer[position++];
    }

    private String readTag() {
        return readTag(false);
    }

    private String readTag(boolean tagAlreadyStarted) {
        String tagName = null;
        char c = read();
        tagMap.clear();
        temp.setLength(0);
        if (!tagAlreadyStarted) {
            while (c != TAG_START && c != NULL) {
                c = read();
            }
        }
        boolean possibleComment = true;
        do {
            c = read();
            if (possibleComment && c == COMMENT) {
                skipComment();
                return readTag();
            } else if (Character.isWhitespace(c)) {
                tagName = readTagWhitespace(tagName);
            } else if (Character.isLetterOrDigit(c) || c == '_' || c == ':') {
                temp.append(c);
            } else {
                if (temp.length() > 0) {
                    if (c == EQUAL) {
                        String name = temp.toString();
                        if (!readString(c)) {
                            return null;
                        }
                        tagMap.put(name, temp.toString());
                        temp.setLength(0);
                    } else {
                        tagName = readTagWhitespace(tagName);
                    }
                }
            }
            if (c == CLOSE) {
                tagMap.put(TAG_MAP_TAG_CLOSE, null);
            }
        } while (c != TAG_END && c != NULL);
        return tagName;
    }

    private void skipComment() {
        char c;
        int i = 0;
        while (true) {
            c = read();
            if (c == '-') {
                i++;
            } else if (c == TAG_END) {
                if (i == 2) {
                    break;
                }
            } else {
                i = 0;
            }
        }
    }

    private String readTagWhitespace(String tagName) {
        if (temp.length() > 0) {
            String st = temp.toString();
            if (tagName == null) {
                tagName = st;
                tagMap.put(TAG_MAP_TAG_NAME, tagName);
            }
            tagMap.put(st, null);
            temp.setLength(0);
        }
        return tagName;
    }

    private boolean readString(char c) {
        while (c != NULL && c != STRING_DELIMITER) {
            c = read();
        }
        c = read();
        temp.setLength(0);
        while (c != NULL && c != STRING_DELIMITER) {
            temp.append(c);
            c = read();
        }
        return true;
    }

    private String readContent(String tagName) {
        char c = read();
        temp.setLength(0);
        principal:
        while (c != NULL) {
            if (c == TAG_START) {
                if ((c = read()) == CLOSE) {
                    while (read() != TAG_END);
                    break principal;
                } else {
                    temp.append(TAG_START);
                    temp.append(c);
                }
            } else {
                temp.append(c);
            }
            c = read();
        }
        return temp.toString();
    }

    public <T> T fillBean(Class<T> bean) {
        try {
            T newType = bean.newInstance();
            fillBean(newType, null);
            return newType;
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InstantiationException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("unchecked")
    private boolean fillBean(Object bean, String tagName) throws IllegalAccessException, InstantiationException {
        XmlMap map = mapper.mapBean(bean);
        if (Types.isNullOrEmpty(tagName)) {
            tagName = readTag();
        }
        if (map.name.equals(tagName)) {
            for (FieldMap field : map.attributes) {
                setFieldValue(bean, field.printable, field.field, tagMap.get(field.name));
            }
            if (tagMap.containsKey(TAG_MAP_TAG_CLOSE)) {
                return true;
            }
            boolean subTagReaded = false;
            while (!isEndTag(tagName, !subTagReaded)) {
                String subTagName = tagMap.get(TAG_MAP_TAG_NAME);
                subTagReaded = false;
                if (map.fieldMap.containsKey(subTagName)) {
                    FieldMap field = map.fieldMap.get(subTagName);
                    if (field.printable) {
                        setFieldValue(bean, field.printable, field.field, readContent(tagName));
                    } else {
                        ComplexType complexType = newComplexType(field.field, bean);
                        if (complexType.list == null) {
                            Object newType = complexType.type.newInstance();
                            fillBean(newType, subTagName);
                            setFieldValue(bean, field.printable, field.field, newType);
                        } else {
                            boolean objectFilled;
                            do {
                                objectFilled = false;
                                Object newType = complexType.type.newInstance();
                                if (fillBean(newType, subTagName)) {
                                    complexType.list.add(newType);
                                    String newSubTagName = readTag();
                                    if (subTagName.equals(newSubTagName)) {
                                        objectFilled = true;
                                    }
                                }
                            } while (objectFilled);
                            subTagReaded = true;
                        }
                    }
                } else {
                    // busca o final da tag descartada
                	skipTag(subTagName);
                }
            }
            return true;
        }
        return false;
    }

	private void skipTag(String subTagName) {
		if (logger.isLoggable(Level.FINE)) {
			StringBuilder sb = new StringBuilder();
			sb.append("Procurando final da tag " + subTagName + "\n");
			for (Entry<String,String> entry : tagMap.entrySet()) {
				sb.append("\n");
				sb.append(entry.getKey());
				sb.append("=");
				sb.append(entry.getValue());
			}
			logger.fine(sb.toString());
		}
		while (!isEndTag(subTagName, true)) {
			if (logger.isLoggable(Level.FINE)) {
				logger.fine(" tag atual " + tagMap.get(TAG_MAP_TAG_NAME));
			}
		}
		if (logger.isLoggable(Level.FINE)) {
			StringBuilder sb = new StringBuilder();
			sb.append("Final da tag  " + subTagName + "\n");
			for (Entry<String,String> entry : tagMap.entrySet()) {
				sb.append("\n");
				sb.append(entry.getKey());
				sb.append("=");
				sb.append(entry.getValue());
			}
			logger.fine(sb.toString());
		}
	}

    private boolean isEndTag(String tagName, boolean canReadTag) {
    	try {
    		if (tagMap.containsKey(TAG_MAP_TAG_CLOSE) && tagName.equals(tagMap.get(TAG_MAP_TAG_NAME))) {
    			return true;
    		} else if (canReadTag) {
		        String s = readTag();
		        if (s != null && s.equals(tagName)) {
		            return tagMap.containsKey(TAG_MAP_TAG_CLOSE);
		        }
    		}
    		return false;
    	} catch (RuntimeException e) {
    		StringBuilder sb = new StringBuilder();
    		sb.append("Tag " + tagName + " not closed.\nLast tag info:");
    		for (Entry<String,String> entry : tagMap.entrySet()) {
    			sb.append("\n");
    			sb.append(entry.getKey());
    			sb.append("=");
    			sb.append(entry.getValue());
    		}
    		logger.severe(sb.toString());
    		throw new RuntimeException("Tag " + tagName + " not closed", e);
    	}
    }

    @SuppressWarnings("unchecked")
    private ComplexType newComplexType(Field field, Object instance) throws IllegalAccessException, InstantiationException {
        Class<?> clazz = field.getType();
        ComplexType complexType = new ComplexType();
        if (clazz.equals(List.class)) {
            ParameterizedType type = null;
            Type listType = null;
            try {
                type = (ParameterizedType) field.getGenericType();
                listType = type.getActualTypeArguments()[0];
            } catch (Exception e) {
                throw new RuntimeException("List has no defined type");
            }
            complexType.type = (Class<?>) listType;
            Object o = field.get(instance);
            if (o == null) {
                complexType.list = new ArrayList();
                setFieldValue(instance, false, field, complexType.list);
            } else {
                complexType.list = (List) o;
            }
        } else {
            complexType.type = clazz;
        }
        return complexType;
    }

    void setFieldValue(Object bean, boolean cast, Field field, Object value) {
        value = cast ? Types.cast(value, field.getType()) : value;
        try {
            field.set(bean, value);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException(e.toString());
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            throw new RuntimeException(e.toString());
        }
    }
}
